#region References
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
#endregion

namespace Ultima
{
	public sealed class TileMatrix
	{
		private readonly HuedTile[][][][][] m_StaticTiles;
		private readonly Tile[][][] m_LandTiles;
		private bool[][] m_RemovedStaticBlock;
		private List<StaticTile>[][] m_StaticTiles_ToAdd;

		public static Tile[] InvalidLandBlock { get; private set; }
		public static HuedTile[][][] EmptyStaticBlock { get; private set; }

		private FileStream m_Map;
		private BinaryReader m_UOPReader;
		private FileStream m_Statics;
		private Entry3D[] m_StaticIndex;

		public Entry3D[] StaticIndex
		{
			get
			{
				if (!StaticIndexInit)
				{
					InitStatics();
				}
				return m_StaticIndex;
			}
		}

		public bool StaticIndexInit;

		public TileMatrixPatch Patch { get; private set; }

		public int BlockWidth { get; private set; }

		public int BlockHeight { get; private set; }

		public int Width { get; private set; }

		public int Height { get; private set; }

		private readonly string mapPath;
		private readonly string indexPath;
		private readonly string staticsPath;

		public void CloseStreams()
		{
			if (m_Map != null)
			{
				m_Map.Close();
			}

			if (m_UOPReader != null)
			{
				m_UOPReader.Close();
			}

			if (m_Statics != null)
			{
				m_Statics.Close();
			}
		}

		public TileMatrix(int fileIndex, int mapID, int width, int height, string path)
		{
			Width = width;
			Height = height;
			BlockWidth = width >> 3;
			BlockHeight = height >> 3;

			if (path == null)
			{
				mapPath = Files.GetFilePath("map{0}.mul", fileIndex);

				if (String.IsNullOrEmpty(mapPath) || !File.Exists(mapPath))
				{
					mapPath = Files.GetFilePath("map{0}LegacyMUL.uop", fileIndex);
				}

				if (mapPath != null && mapPath.EndsWith(".uop"))
				{
					IsUOPFormat = true;
				}
			}
			else
			{
				mapPath = Path.Combine(path, String.Format("map{0}.mul", fileIndex));

				if (!File.Exists(mapPath))
				{
					mapPath = Path.Combine(path, String.Format("map{0}LegacyMUL.uop", fileIndex));
				}

				if (!File.Exists(mapPath))
				{
					mapPath = null;
				}
				else if (mapPath != null && mapPath.EndsWith(".uop"))
				{
					IsUOPFormat = true;
				}
			}

			if (path == null)
			{
				indexPath = Files.GetFilePath("staidx{0}.mul", fileIndex);
			}
			else
			{
				indexPath = Path.Combine(path, String.Format("staidx{0}.mul", fileIndex));
				if (!File.Exists(indexPath))
				{
					indexPath = null;
				}
			}

			if (path == null)
			{
				staticsPath = Files.GetFilePath("statics{0}.mul", fileIndex);
			}
			else
			{
				staticsPath = Path.Combine(path, String.Format("statics{0}.mul", fileIndex));
				if (!File.Exists(staticsPath))
				{
					staticsPath = null;
				}
			}

			EmptyStaticBlock = new HuedTile[8][][];

			for (int i = 0; i < 8; ++i)
			{
				EmptyStaticBlock[i] = new HuedTile[8][];

				for (int j = 0; j < 8; ++j)
				{
					EmptyStaticBlock[i][j] = new HuedTile[0];
				}
			}

			InvalidLandBlock = new Tile[196];

			m_LandTiles = new Tile[BlockWidth][][];
			m_StaticTiles = new HuedTile[BlockWidth][][][][];

			Patch = new TileMatrixPatch(this, mapID, path);
		}

		public void SetStaticBlock(int x, int y, HuedTile[][][] value)
		{
			if (x < 0 || y < 0 || x >= BlockWidth || y >= BlockHeight)
			{
				return;
			}

			if (m_StaticTiles[x] == null)
			{
				m_StaticTiles[x] = new HuedTile[BlockHeight][][][];
			}

			m_StaticTiles[x][y] = value;
		}

		public HuedTile[][][] GetStaticBlock(int x, int y)
		{
			return GetStaticBlock(x, y, true);
		}

		public HuedTile[][][] GetStaticBlock(int x, int y, bool patch)
		{
			if (x < 0 || y < 0 || x >= BlockWidth || y >= BlockHeight)
			{
				return EmptyStaticBlock;
			}

			if (m_StaticTiles[x] == null)
			{
				m_StaticTiles[x] = new HuedTile[BlockHeight][][][];
			}

			HuedTile[][][] tiles = m_StaticTiles[x][y];

			if (tiles == null)
			{
				tiles = m_StaticTiles[x][y] = ReadStaticBlock(x, y);
			}

			if ((Map.UseDiff) && (patch))
			{
				if (Patch.StaticBlocksCount > 0)
				{
					if (Patch.StaticBlocks[x] != null)
					{
						if (Patch.StaticBlocks[x][y] != null)
						{
							tiles = Patch.StaticBlocks[x][y];
						}
					}
				}
			}
			return tiles;
		}

		public HuedTile[] GetStaticTiles(int x, int y, bool patch)
		{
			return GetStaticBlock(x >> 3, y >> 3, patch)[x & 0x7][y & 0x7];
		}

		public HuedTile[] GetStaticTiles(int x, int y)
		{
			return GetStaticBlock(x >> 3, y >> 3)[x & 0x7][y & 0x7];
		}

		public void SetLandBlock(int x, int y, Tile[] value)
		{
			if (x < 0 || y < 0 || x >= BlockWidth || y >= BlockHeight)
			{
				return;
			}

			if (m_LandTiles[x] == null)
			{
				m_LandTiles[x] = new Tile[BlockHeight][];
			}

			m_LandTiles[x][y] = value;
		}

		public Tile[] GetLandBlock(int x, int y)
		{
			return GetLandBlock(x, y, true);
		}

		public Tile[] GetLandBlock(int x, int y, bool patch)
		{
			if (x < 0 || y < 0 || x >= BlockWidth || y >= BlockHeight)
			{
				return InvalidLandBlock;
			}

			if (m_LandTiles[x] == null)
			{
				m_LandTiles[x] = new Tile[BlockHeight][];
			}

			Tile[] tiles = m_LandTiles[x][y];

			if (tiles == null)
			{
				tiles = m_LandTiles[x][y] = ReadLandBlock(x, y);
			}

			if ((Map.UseDiff) && (patch))
			{
				if (Patch.LandBlocksCount > 0)
				{
					if (Patch.LandBlocks[x] != null)
					{
						if (Patch.LandBlocks[x][y] != null)
						{
							tiles = Patch.LandBlocks[x][y];
						}
					}
				}
			}
			return tiles;
		}

		public Tile GetLandTile(int x, int y, bool patch)
		{
			return GetLandBlock(x >> 3, y >> 3, patch)[((y & 0x7) << 3) + (x & 0x7)];
		}

		public Tile GetLandTile(int x, int y)
		{
			return GetLandBlock(x >> 3, y >> 3)[((y & 0x7) << 3) + (x & 0x7)];
		}

		private void InitStatics()
		{
			m_StaticIndex = new Entry3D[BlockHeight * BlockWidth];
			if (indexPath == null)
			{
				return;
			}
			using (var index = new FileStream(indexPath, FileMode.Open, FileAccess.Read, FileShare.Read))
			{
				m_Statics = new FileStream(staticsPath, FileMode.Open, FileAccess.Read, FileShare.Read);
				var count = (int)(index.Length / 12);
				GCHandle gc = GCHandle.Alloc(m_StaticIndex, GCHandleType.Pinned);
				var buffer = new byte[index.Length];
				index.Read(buffer, 0, (int)index.Length);
				Marshal.Copy(buffer, 0, gc.AddrOfPinnedObject(), (int)Math.Min(index.Length, BlockHeight * BlockWidth * 12));
				gc.Free();
				for (var i = (int)Math.Min(index.Length, BlockHeight * BlockWidth); i < BlockHeight * BlockWidth; ++i)
				{
					m_StaticIndex[i].lookup = -1;
					m_StaticIndex[i].length = -1;
					m_StaticIndex[i].extra = -1;
				}
				StaticIndexInit = true;
			}
		}

		private static HuedTileList[][] m_Lists;
		private static byte[] m_Buffer;

		private unsafe HuedTile[][][] ReadStaticBlock(int x, int y)
		{
			try
			{
				if (!StaticIndexInit)
				{
					InitStatics();
				}
				if (m_Statics == null || !m_Statics.CanRead || !m_Statics.CanSeek)
				{
					if (staticsPath == null)
					{
						m_Statics = null;
					}
					else
					{
						m_Statics = new FileStream(staticsPath, FileMode.Open, FileAccess.Read, FileShare.Read);
					}
				}
				if (m_Statics == null)
				{
					return EmptyStaticBlock;
				}

				int lookup = m_StaticIndex[(x * BlockHeight) + y].lookup;
				int length = m_StaticIndex[(x * BlockHeight) + y].length;

				if (lookup < 0 || length <= 0)
				{
					return EmptyStaticBlock;
				}
				else
				{
					int count = length / 7;

					m_Statics.Seek(lookup, SeekOrigin.Begin);

					if (m_Buffer == null || m_Buffer.Length < length)
					{
						m_Buffer = new byte[length];
					}

					GCHandle gc = GCHandle.Alloc(m_Buffer, GCHandleType.Pinned);
					try
					{
						m_Statics.Read(m_Buffer, 0, length);

						if (m_Lists == null)
						{
							m_Lists = new HuedTileList[8][];

							for (int i = 0; i < 8; ++i)
							{
								m_Lists[i] = new HuedTileList[8];

								for (int j = 0; j < 8; ++j)
								{
									m_Lists[i][j] = new HuedTileList();
								}
							}
						}

						HuedTileList[][] lists = m_Lists;

						for (int i = 0; i < count; ++i)
						{
							var ptr = new IntPtr((long)gc.AddrOfPinnedObject() + i * sizeof(StaticTile));
							var cur = (StaticTile)Marshal.PtrToStructure(ptr, typeof(StaticTile));
							lists[cur.m_X & 0x7][cur.m_Y & 0x7].Add(Art.GetLegalItemID(cur.m_ID), cur.m_Hue, cur.m_Z);
						}

						var tiles = new HuedTile[8][][];

						for (int i = 0; i < 8; ++i)
						{
							tiles[i] = new HuedTile[8][];

							for (int j = 0; j < 8; ++j)
							{
								tiles[i][j] = lists[i][j].ToArray();
							}
						}

						return tiles;
					}
					finally
					{
						gc.Free();
					}
				}
			}
			finally
			{
				//if (m_Statics != null)
				//    m_Statics.Close();
			}
		}

		/* UOP map files support code, written by Wyatt (c) www.ruosi.org
		 * It's not possible if some entry has unknown hash. Throwed exception
		 * means that EA changed maps UOPs again.
		 */

		#region UOP
		public bool IsUOPFormat { get; set; }
		public bool IsUOPAlreadyRead { get; set; }

		private struct UOPFile
		{
			public readonly long Offset;
			public readonly int Length;

			public UOPFile(long offset, int length)
			{
				Offset = offset;
				Length = length;
			}
		}

		private UOPFile[] UOPFiles { get; set; }
		private long UOPLength { get { return m_Map.Length; } }

		private void ReadUOPFiles(string pattern)
		{
			m_UOPReader = new BinaryReader(m_Map);

			m_UOPReader.BaseStream.Seek(0, SeekOrigin.Begin);

			if (m_UOPReader.ReadInt32() != 0x50594D)
			{
				throw new ArgumentException("Bad UOP file.");
			}

			m_UOPReader.ReadInt64(); // version + signature
			long nextBlock = m_UOPReader.ReadInt64();
			m_UOPReader.ReadInt32(); // block capacity
			int count = m_UOPReader.ReadInt32();

			UOPFiles = new UOPFile[count];

			var hashes = new Dictionary<ulong, int>();

			for (int i = 0; i < count; i++)
			{
				string file = string.Format("build/{0}/{1:D8}.dat", pattern, i);
				ulong hash = FileIndex.HashFileName(file);

				if (!hashes.ContainsKey(hash))
				{
					hashes.Add(hash, i);
				}
			}

			m_UOPReader.BaseStream.Seek(nextBlock, SeekOrigin.Begin);

			do
			{
				int filesCount = m_UOPReader.ReadInt32();
				nextBlock = m_UOPReader.ReadInt64();

				for (int i = 0; i < filesCount; i++)
				{
					long offset = m_UOPReader.ReadInt64();
					int headerLength = m_UOPReader.ReadInt32();
					int compressedLength = m_UOPReader.ReadInt32();
					int decompressedLength = m_UOPReader.ReadInt32();
					ulong hash = m_UOPReader.ReadUInt64();
					m_UOPReader.ReadUInt32(); // Adler32
					short flag = m_UOPReader.ReadInt16();

					int length = flag == 1 ? compressedLength : decompressedLength;

					if (offset == 0)
					{
						continue;
					}

					int idx;
					if (hashes.TryGetValue(hash, out idx))
					{
						if (idx < 0 || idx > UOPFiles.Length)
						{
							throw new IndexOutOfRangeException("hashes dictionary and files collection have different count of entries!");
						}

						UOPFiles[idx] = new UOPFile(offset + headerLength, length);
					}
					else
					{
						throw new ArgumentException(
							string.Format("File with hash 0x{0:X8} was not found in hashes dictionary! EA Mythic changed UOP format!", hash));
					}
				}
			}
			while (m_UOPReader.BaseStream.Seek(nextBlock, SeekOrigin.Begin) != 0);
		}

		private long CalculateOffsetFromUOP(long offset)
		{
			long pos = 0;

			foreach (UOPFile t in UOPFiles)
			{
				long currPos = pos + t.Length;

				if (offset < currPos)
				{
					return t.Offset + (offset - pos);
				}

				pos = currPos;
			}

			return UOPLength;
		}
		#endregion

		private Tile[] ReadLandBlock(int x, int y)
		{
			if (m_Map == null || !m_Map.CanRead || !m_Map.CanSeek)
			{
				if (mapPath == null)
				{
					m_Map = null;
				}
				else
				{
					m_Map = new FileStream(mapPath, FileMode.Open, FileAccess.Read, FileShare.Read);
				}

				if (IsUOPFormat && mapPath != null && !IsUOPAlreadyRead)
				{
					var fi = new FileInfo(mapPath);
					string uopPattern = fi.Name.Replace(fi.Extension, "").ToLowerInvariant();

					ReadUOPFiles(uopPattern);
					IsUOPAlreadyRead = true;
				}
			}
			var tiles = new Tile[64];
			if (m_Map != null)
			{
				long offset = ((x * BlockHeight) + y) * 196 + 4;

				if (IsUOPFormat)
				{
					offset = CalculateOffsetFromUOP(offset);
				}

				m_Map.Seek(offset, SeekOrigin.Begin);

				GCHandle gc = GCHandle.Alloc(tiles, GCHandleType.Pinned);
				try
				{
					if (m_Buffer == null || m_Buffer.Length < 192)
					{
						m_Buffer = new byte[192];
					}

					m_Map.Read(m_Buffer, 0, 192);

					Marshal.Copy(m_Buffer, 0, gc.AddrOfPinnedObject(), 192);
				}
				finally
				{
					gc.Free();
				}
				//m_Map.Close();
			}

			return tiles;
		}

		public void RemoveStaticBlock(int blockx, int blocky)
		{
			if (m_RemovedStaticBlock == null)
			{
				m_RemovedStaticBlock = new bool[BlockWidth][];
			}
			if (m_RemovedStaticBlock[blockx] == null)
			{
				m_RemovedStaticBlock[blockx] = new bool[BlockHeight];
			}
			m_RemovedStaticBlock[blockx][blocky] = true;
			if (m_StaticTiles[blockx] == null)
			{
				m_StaticTiles[blockx] = new HuedTile[BlockHeight][][][];
			}
			m_StaticTiles[blockx][blocky] = EmptyStaticBlock;
		}

		public bool IsStaticBlockRemoved(int blockx, int blocky)
		{
			if (m_RemovedStaticBlock == null)
			{
				return false;
			}
			if (m_RemovedStaticBlock[blockx] == null)
			{
				return false;
			}
			return m_RemovedStaticBlock[blockx][blocky];
		}

		public bool PendingStatic(int blockx, int blocky)
		{
			if (m_StaticTiles_ToAdd == null)
			{
				return false;
			}
			if (m_StaticTiles_ToAdd[blocky] == null)
			{
				return false;
			}
			if (m_StaticTiles_ToAdd[blocky][blockx] == null)
			{
				return false;
			}
			return true;
		}

		public void AddPendingStatic(int blockx, int blocky, StaticTile toadd)
		{
			if (m_StaticTiles_ToAdd == null)
			{
				m_StaticTiles_ToAdd = new List<StaticTile>[BlockHeight][];
			}
			if (m_StaticTiles_ToAdd[blocky] == null)
			{
				m_StaticTiles_ToAdd[blocky] = new List<StaticTile>[BlockWidth];
			}
			if (m_StaticTiles_ToAdd[blocky][blockx] == null)
			{
				m_StaticTiles_ToAdd[blocky][blockx] = new List<StaticTile>();
			}
			m_StaticTiles_ToAdd[blocky][blockx].Add(toadd);
		}

		public StaticTile[] GetPendingStatics(int blockx, int blocky)
		{
			if (m_StaticTiles_ToAdd == null)
			{
				return null;
			}
			if (m_StaticTiles_ToAdd[blocky] == null)
			{
				return null;
			}
			if (m_StaticTiles_ToAdd[blocky][blockx] == null)
			{
				return null;
			}

			return m_StaticTiles_ToAdd[blocky][blockx].ToArray();
		}

		public void Dispose()
		{
			if (m_Map != null)
			{
				m_Map.Close();
			}

			if (m_UOPReader != null)
			{
				m_UOPReader.Close();
			}

			if (m_Statics != null)
			{
				m_Statics.Close();
			}
		}
	}

	[StructLayout(LayoutKind.Sequential, Pack = 1)]
	public struct StaticTile
	{
		public ushort m_ID;
		public byte m_X;
		public byte m_Y;
		public sbyte m_Z;
		public short m_Hue;
	}

	[StructLayout(LayoutKind.Sequential, Pack = 1)]
	public struct HuedTile
	{
		internal sbyte m_Z;
		internal ushort m_ID;
		internal int m_Hue;

		public ushort ID { get { return m_ID; } set { m_ID = value; } }
		public int Hue { get { return m_Hue; } set { m_Hue = value; } }
		public int Z { get { return m_Z; } set { m_Z = (sbyte)value; } }

		public HuedTile(ushort id, short hue, sbyte z)
		{
			m_ID = id;
			m_Hue = hue;
			m_Z = z;
		}

		public void Set(ushort id, short hue, sbyte z)
		{
			m_ID = id;
			m_Hue = hue;
			m_Z = z;
		}
	}

	public struct MTile : IComparable
	{
		internal ushort m_ID;
		internal sbyte m_Z;
        internal TileFlag m_Flag;
		internal int m_Solver;

		public ushort ID { get { return m_ID; } }
		public int Z { get { return m_Z; } set { m_Z = (sbyte)value; } }

        public TileFlag Flag { get { return m_Flag; } set { m_Flag = value; } }
		public int Solver { get { return m_Solver; } set { m_Solver = value; } }

		public MTile(ushort id, sbyte z)
		{
			m_ID = Art.GetLegalItemID(id);
			m_Z = z;
            m_Flag = TileFlag.Background;
			m_Solver = 0;
		}

        public MTile(ushort id, sbyte z, TileFlag flag)
		{
			m_ID = Art.GetLegalItemID(id);
			m_Z = z;
			m_Flag = flag;
			m_Solver = 0;
		}

		public void Set(ushort id, sbyte z)
		{
			m_ID = Art.GetLegalItemID(id);
			m_Z = z;
		}

        public void Set(ushort id, sbyte z, TileFlag flag)
		{
			m_ID = Art.GetLegalItemID(id);
			m_Z = z;
			m_Flag = flag;
		}

		public int CompareTo(object x)
		{
			if (x == null)
			{
				return 1;
			}

			if (!(x is MTile))
			{
				throw new ArgumentNullException();
			}

			var a = (MTile)x;

			ItemData ourData = TileData.ItemTable[m_ID];
			ItemData theirData = TileData.ItemTable[a.ID];

			int ourTreshold = 0;
			if (ourData.Height > 0)
			{
				++ourTreshold;
			}
			if (!ourData.Background)
			{
				++ourTreshold;
			}
			int ourZ = Z;
			int theirTreshold = 0;
			if (theirData.Height > 0)
			{
				++theirTreshold;
			}
			if (!theirData.Background)
			{
				++theirTreshold;
			}
			int theirZ = a.Z;

			ourZ += ourTreshold;
			theirZ += theirTreshold;
			int res = ourZ - theirZ;
			if (res == 0)
			{
				res = ourTreshold - theirTreshold;
			}
			if (res == 0)
			{
				res = m_Solver - a.Solver;
			}
			return res;
		}
	}

	[StructLayout(LayoutKind.Sequential, Pack = 1)]
	public struct Tile : IComparable
	{
		internal ushort m_ID;
		internal sbyte m_Z;

		public ushort ID { get { return m_ID; } }
		public int Z { get { return m_Z; } set { m_Z = (sbyte)value; } }

		public Tile(ushort id, sbyte z)
		{
			m_ID = id;
			m_Z = z;
		}

		public Tile(ushort id, sbyte z, sbyte flag)
		{
			m_ID = id;
			m_Z = z;
		}

		public void Set(ushort id, sbyte z)
		{
			m_ID = id;
			m_Z = z;
		}

		public void Set(ushort id, sbyte z, sbyte flag)
		{
			m_ID = id;
			m_Z = z;
		}

		public int CompareTo(object x)
		{
			if (x == null)
			{
				return 1;
			}

			if (!(x is Tile))
			{
				throw new ArgumentNullException();
			}

			var a = (Tile)x;

			if (m_Z > a.m_Z)
			{
				return 1;
			}
			else if (a.m_Z > m_Z)
			{
				return -1;
			}

			ItemData ourData = TileData.ItemTable[m_ID];
			ItemData theirData = TileData.ItemTable[a.m_ID];

			if (ourData.Height > theirData.Height)
			{
				return 1;
			}
			else if (theirData.Height > ourData.Height)
			{
				return -1;
			}

			if (ourData.Background && !theirData.Background)
			{
				return -1;
			}
			else if (theirData.Background && !ourData.Background)
			{
				return 1;
			}

			return 0;
		}
	}
}